/*
 * Galaxium Messenger
 * Copyright (C) 2008 Paul Burton <paulburton89@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;

using Anculus.Core;

using Galaxium.Core;

namespace Galaxium.Protocol.Msn
{
	public partial class MsnP2PSession
	{
		const int _directNegotiationTimeout = 10000;
		
		uint _directNegotiationTimer;
		int _mappedPort = 0;
		
		internal void SendDirectInvite ()
		{
			// Only send the direct invite if we're currently using an SBBridge
			if (!(_bridge is SBBridge))
				return;
			
			SLPRequestMessage slp = new SLPRequestMessage (Remote, "INVITE");
			
			slp.ContentType = "application/x-msnmsgr-transreqbody";
			slp.CallID = _invite.CallID;
			
			slp.MIMEBody["Bridges"] = "TCPv1 SBBridge";
			slp.MIMEBody["ICF"] = "false";
			
			slp.MIMEBody["UPnPNat"] = NetworkUtility.UseNAT.ToString ().ToLower ();
			
			if (NetworkUtility.IPAddress == null)
			{
				slp.MIMEBody["Conn-Type"] = "Unknown-Connect";
				slp.MIMEBody["TCP-Conn-Type"] = "Unknown-Connect";
				slp.MIMEBody["NetID"] = new Random ().Next ().ToString ();
			}
			else if (NetworkUtility.UseNAT)
			{
				slp.MIMEBody["Conn-Type"] = "Port-Restrict-NAT";
				slp.MIMEBody["TCP-Conn-Type"] = "Port-Restrict-NAT";
				slp.MIMEBody["NetID"] = new Random ().Next ().ToString ();
			}
			else
			{
				slp.MIMEBody["Conn-Type"] = "Direct-Connect";
				slp.MIMEBody["TCP-Conn-Type"] = "Direct-Connect";
				slp.MIMEBody["NetID"] = "0";
			}
			
			//Log.Debug ("Direct Invite:\n{0}", System.Text.Encoding.UTF8.GetString (slp.ToByteArray ()));
			
			Send (slp, delegate { });
			
			_directNegotiationTimer = TimerUtility.RequestCallback (DirectNegotiationTimedOut, _directNegotiationTimeout);
			
			// Stop sending until we receive a response to the direct invite
			// or the timeout expires
			_bridge.StopSending (this);
		}
		
		void ProcessDirectInvite (P2PMessage msg)
		{
			SLPMessage slp = msg.SLPMessage;
			
			if (slp.ContentType == "application/x-msnmsgr-transreqbody")
				ProcessDirectReqInvite (slp);
			else if (slp.ContentType == "application/x-msnmsgr-transrespbody")
				ProcessDirectRespInvite (slp);
		}
		
		void ProcessDirectReqInvite (SLPMessage slp)
		{
			string[] bridges = slp.MIMEBody["Bridges"].Value.Split (new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
			
			// Check the remote client supports the TCP bridge
			if (Array.IndexOf (bridges, "TCPv1") < 0)
			{
				DirectNegotiationFailed ();
				return;
			}
			
			SLPStatusMessage okSlp = new SLPStatusMessage (_remote, 200, "OK");
			okSlp.Branch = slp.Branch;
			okSlp.CallID = slp.CallID;
			okSlp.CSeq = slp.CSeq + 1;
			okSlp.ContentType = "application/x-msnmsgr-transrespbody";
			okSlp.MIMEBody["SessionID"] = _sessionID.ToString ();
			okSlp.MIMEBody["Bridge"] = "TCPv1";
			
			bool listen = (NetworkUtility.IPAddress != null) && !NetworkUtility.UseNAT;
			
			Log.Debug ("Using NAT: {0}", NetworkUtility.UseNAT);
			
			okSlp.MIMEBody["Listening"] = listen.ToString ().ToLower ();
			
			if (!listen)
			{
				okSlp.MIMEBody["Nonce"] = new Guid ().ToString ("B");
				
				okSlp.MIMEBody["Conn-Type"] = "Port-Restrict-NAT";
				okSlp.MIMEBody["TCP-Conn-Type"] = "Port-Restrict-NAT";
			}
			else
			{
				IPAddress ip = NetworkUtility.IPAddress;
				int port = NetworkUtility.GetUnusedPort ();
				Guid nonce = Guid.NewGuid ();
				
				int externalPort;
				if (!NetworkUtility.CreatePortMap (port, out externalPort))
					externalPort = port;
				else
				{
					Log.Debug ("Mapped internal port {0} to external port {1}", port, externalPort);
					_mappedPort = port;
				}
				
				okSlp.MIMEBody["Nonce"] = nonce.ToString ("B");
				
				if (NetworkUtility.UseNAT)
				{
					okSlp.MIMEBody["IPv4Internal-Addrs"] = NetworkUtility.LocalIPAddress.ToString ();
					okSlp.MIMEBody["IPv4Internal-Port"] = port.ToString ();
					
					okSlp.MIMEBody["IPv4External-Addrs"] = ip.ToString ();
					okSlp.MIMEBody["IPv4External-Port"] = port.ToString ();
				}
				else
				{
					okSlp.MIMEBody["IPv4Internal-Addrs"] = ip.ToString ();
					okSlp.MIMEBody["IPv4Internal-Port"] = port.ToString ();
				}
				
				Remote.DirectBridge = new TCPv1Bridge (Session, true, ip, port, nonce);
			}
			
			_directNegotiationTimer = TimerUtility.RequestCallback (DirectNegotiationTimedOut, _directNegotiationTimeout);
			
			Send (okSlp, delegate { });
		}
		
		void ProcessDirectRespInvite (SLPMessage slp)
		{
			if (slp.MIMEBody["Bridge"].Value != "TCPv1")
			{
				Log.Error ("Remote client wants to use bridge '{0}', only TCPv1 is supported", slp.MIMEBody["Bridge"].Value);
				DirectNegotiationFailed ();
				return;
			}
			
			if (slp.MIMEBody["Listening"].Value != "true")
			{
				Log.Error ("Remote client is not listening");
				DirectNegotiationFailed ();
				return;
			}
			
			Log.Debug ("SLP MIME Body:\n{0}", slp.MIMEBody.ToString ());
			
			Guid nonce = new Guid (slp.MIMEBody["Nonce"].Value);
			
			string[] addrs = new string [0];
			List<IPAddress> ipAddrs = new List<IPAddress> ();
			int port = 0;
			
			if (slp.MIMEBody.ContainsKey ("IPv4External-Addrs"))
			{
				Log.Debug ("Using external IP addresses");
				
				addrs = slp.MIMEBody["IPv4External-Addrs"].Value.Split (new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
				port = int.Parse (slp.MIMEBody["IPv4External-Port"].Value);
				
				Log.Debug ("{0} external IP addresses found, with port {1}", addrs.Length, port);
				
				for (int i = 0; i < addrs.Length; i++)
				{
					IPAddress ip;
					if (IPAddress.TryParse (addrs[i], out ip))
					{
						Log.Debug ("\t{0}", addrs[i]);
						ipAddrs.Add (ip);
						
						if (ip.Equals (NetworkUtility.IPAddress))
						{
							Log.Debug ("External IP matches our own, clearing external IPs");
							addrs = new string [0];
							ipAddrs.Clear ();
							break;
						}
					}
					else
						Log.Debug ("\tUnable to parse {0}", addrs[i]);
				}
			}
			
			if ((ipAddrs.Count == 0) && slp.MIMEBody.ContainsKey ("IPv4Internal-Addrs"))
			{
				Log.Debug ("Using internal IP addresses");
				
				addrs = slp.MIMEBody["IPv4Internal-Addrs"].Value.Split (new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
				port = int.Parse (slp.MIMEBody["IPv4Internal-Port"].Value);
			}
			
			if (addrs.Length == 0)
			{
				Log.Error ("Unable to find any remote IP addressed");
				DirectNegotiationFailed ();
				return;
			}
			
			Log.Debug ("{0} IP addresses found, with port {1}", addrs.Length, port);
			
			for (int i = 0; i < addrs.Length; i++)
			{
				IPAddress ip;
				if (IPAddress.TryParse (addrs[i], out ip))
				{
					Log.Debug ("\t{0}", addrs[i]);
					ipAddrs.Add (ip);
				}
				else
					Log.Debug ("\tUnable to parse {0}", addrs[i]);
			}
			
			if (ipAddrs.Count == 0)
			{
				Log.Error ("Unable to parse any remote IP addresses");
				DirectNegotiationFailed ();
				return;
			}
			
			// Try to find the correct IP
			IPAddress ipAddr = null;
			byte[] localBytes = NetworkUtility.LocalIPAddress.GetAddressBytes ();
			
			foreach (IPAddress ip in ipAddrs)
			{
				if (ip.AddressFamily == AddressFamily.InterNetwork)
				{
					// This is an IPv4 address
					// Check if the first 3 octets match our local IP address
					// If so, make use of that address (it's on our LAN)
					
					byte[] bytes = ip.GetAddressBytes ();
					
					if ((bytes[0] == localBytes[0]) && (bytes[1] == localBytes[1]) && (bytes[2] == localBytes[2]))
					{
						ipAddr = ip;
						break;
					}
				}
			}
			
			if (ipAddr == null)
				ipAddr = ipAddrs[0];
			
			Log.Debug ("Using remote IP {0}", ipAddr);
			
			Remote.DirectBridge = new TCPv1Bridge (this, false, ipAddr, port, nonce);
		}
		
		void DirectNegotiationSuccessful ()
		{
			Log.Debug ("Direct connection negotiation was successful");
			
			TimerUtility.RemoveCallback (_directNegotiationTimer);
			_directNegotiationTimer = 0;
		}
		
		void DirectNegotiationFailed ()
		{
			Log.Debug ("Direct connection negotiation was unsuccessful");
			
			if (_directNegotiationTimer != 0)
			{
				TimerUtility.RemoveCallback (_directNegotiationTimer);
				_directNegotiationTimer = 0;
			}
			
			_bridge.ResumeSending (this);
		}
		
		void DirectNegotiationTimedOut ()
		{
			Log.Debug ("Direct connection negotiation timed out");
			
			DirectNegotiationFailed ();
		}
		
		void UnmapDirectPort ()
		{
			if (_mappedPort > 0)
			{
				Log.Debug ("Unmapping mapped port {0}", _mappedPort);
				
				NetworkUtility.DeletePortMap (_mappedPort);
				_mappedPort = 0;
			}
		}
	}
}
